[runtime] Reduce registered peer storage allocations - #12657
Conversation
Store the first registered peer inline and allocate a collision list only when identity hashes collide. Add BenchmarkDotNet coverage for registration, lookup, and collision churn across the candidate layouts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The runtime changes appear internally consistent and safe, and the only feedback is minor benchmark-helper API contract tightening.
Review tier: Lite
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
tests/Xamarin.Android.Tools.Benchmarks/RegisteredPeersBenchmarks.cs — 💡 suggestion: This Peek implementation returns null on misses even though the method return… |
What changed in this PR
Reduces allocation overhead in the runtime’s registered-peer registry by inlining the first ReferenceTrackingHandle per identity-hash bucket and only allocating a list on actual collisions, while also adding BenchmarkDotNet benchmarks to measure registration/lookup/churn behavior.
Changes:
- Switch
JavaMarshalRegisteredPeersstorage fromDictionary<int, List<ReferenceTrackingHandle>>toDictionary<int, RegisteredPeerBucket>and synchronize via a dedicatedLock. - Factor peer replacement/keep logic into
ReconcilePeer()and ensure struct-bucket mutations are written back into the dictionary. - Add BenchmarkDotNet coverage for add/peek/remove-add churn scenarios across multiple storage strategies.
| File | Description |
|---|---|
| tests/Xamarin.Android.Tools.Benchmarks/RegisteredPeersBenchmarks.cs | Adds new microbenchmarks and reference implementations comparing bucket storage strategies. |
| src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs | Implements inline-first bucket storage + dedicated lock for registered peers in the runtime. |
Suppressed comments (3)
tests/Xamarin.Android.Tools.Benchmarks/RegisteredPeersBenchmarks.cs:311
- 💡 suggestion: This
Peekmethod can returnnulleven though it returnsPeer. If a miss is unexpected in this benchmark, throwing makes failures explicit without changing the hot path.
public Peer Peek (Peer expected)
{
if (!peers.TryGetValue (expected.Hash, out FirstRestBucket values))
return null;
tests/Xamarin.Android.Tools.Benchmarks/RegisteredPeersBenchmarks.cs:414
- 💡 suggestion:
Peekreturnsnullon misses (including the single-handle fast path) even though the return type isPeer. Throwing on misses keeps the method contract non-null and surfaces setup errors clearly.
public Peer Peek (Peer expected)
{
if (!peers.TryGetValue (expected.Hash, out object values))
return null;
tests/Xamarin.Android.Tools.Benchmarks/RegisteredPeersBenchmarks.cs:476
- 💡 suggestion: This
Peekimplementation returnsnullon misses while returningPeer. If misses are not expected in the benchmark harness, throwing makes failures explicit and avoids a misleading non-null signature.
public Peer Peek (Peer expected)
{
if (!peers.TryGetValue (expected.Hash, out InlineFirstRestBucket values))
return null;
|
/review |
|
✅ Android PR Reviewer completed successfully!
|
There was a problem hiding this comment.
✅ LGTM
Findings: 0 errors · 0 warnings · 1 suggestion
The runtime bucket refactor preserves peer reconciliation, handle disposal, locking, and the required value-type dictionary write-backs. The dedicated lock also avoids coupling synchronization to the registry implementation. All 45 CI checks are green. I left one non-blocking inline suggestion to make the collision lookup benchmark exercise first/middle entries instead of only the reverse-scan fast case.
Generated by Android PR Reviewer for #12657 · gpt56 · 107.6 AIC · ⌖ 8.86 AIC · ⊞ 25.7K
Comment /review to run again

Summary
ReferenceTrackingHandleinline in each registered-peer dictionary entryList<ReferenceTrackingHandle>only for actual JNI identity-hash collisionsLockfor registry synchronizationBenchmarks
BenchmarkDotNet 0.15.8, .NET 11 Arm64 RyuJIT, Apple M5 Max.
Add
Peek
Collision remove/add churn
The churn case intentionally uses four peers for every identity hash. Real collision buckets are expected to be extremely rare and shallow; the singleton path avoids both boxing and list allocation.
Run with:
dotnet run --project tests/Xamarin.Android.Tools.Benchmarks/Xamarin.Android.Tools.Benchmarks.csproj -c Release -- --filter '*RegisteredPeers*'